Flexbox와 Grid를 선택하는 기준
Flexbox와 Grid를 선택하는 기준
Flexbox는 한 축을 따라 항목의 크기와 남은 공간을 분배하는 데 강하고, Grid는 행과 열의 track을 먼저 정의해 여러 항목의 정렬 관계를 유지하는 데 강하다. “작은 UI는 Flexbox, 큰 화면은 Grid”가 아니라 콘텐츠가 흐름을 결정하는가, 컨테이너가 행·열 구조를 결정하는가를 기준으로 선택한다. 실제 화면에서는 Grid 안의 Flexbox처럼 둘을 중첩하는 경우가 자연스럽다.
Flexbox와 Grid는 비슷한 결과를 만들 수 있다. 카드 세 개를 가로로 놓는 정도는 어느 쪽으로도 구현 가능하다.
.cards {
display: flex;
gap: 1rem;
}
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
차이는 세 장일 때보다 콘텐츠가 늘고, 줄이 바뀌고, 제목 길이가 달라질 때 나타난다. 레이아웃 도구를 문법 취향으로 고르면 예외 규칙이 쌓이지만, 브라우저가 공간을 계산하는 방향을 기준으로 고르면 CSS가 단순해진다.
목차
- #한 축과 두 축이라는 설명의 실제 의미
- #콘텐츠 중심과 레이아웃 중심으로 판단하기
- #Flexbox가 자연스러운 경우
- #Grid가 자연스러운 경우
- #여러 줄 Flexbox와 Grid의 결정적인 차이
- #남은 공간과 항목 크기가 계산되는 방식
- #자주 만나는 overflow 문제
- #반응형 카드 목록 설계하기
- #Grid 안에 Flexbox를 중첩하기
- #시각적 순서와 DOM 순서를 분리하지 않기
- #선택을 위한 실무 체크리스트
- #마무리
- #관련 노트
- #참고 자료
한 축과 두 축이라는 설명의 실제 의미
Flexbox는 main axis 하나를 기준으로 항목의 크기와 남은 공간을 계산한다. flex-direction: row라면 가로가 main axis이고 세로가 cross axis다.
flowchart LR
A["Flex item 1"] --> B["Flex item 2"]
B --> C["Flex item 3"]justify-content는 main axis의 남은 공간을 배분하고 align-items는 cross axis의 정렬을 다룬다. flex-direction: column으로 바꾸면 두 속성이 작동하는 축도 바뀐다.
Grid는 컨테이너에 행과 열 track을 정의한다.
column 1 column 2 column 3
┌──────────┬──────────────┬──────────┐
│ item A │ item B │ item C │ row 1
├──────────┼──────────────┼──────────┤
│ item D │ item E │ item F │ row 2
└──────────┴──────────────┴──────────┘
두 번째 행의 항목은 첫 번째 행이 만든 열 선을 공유한다. 반면 wrapping된 Flexbox에서는 각 줄이 별도의 flex container처럼 공간을 배분한다.
아주 작은 버튼 그룹도 행과 열의 관계를 동시에 제어한다면 Grid가 자연스러울 수 있다. 반대로 페이지 전체가 세로 흐름 하나라면 큰 레이아웃에도 Flexbox가 적합할 수 있다.
콘텐츠 중심과 레이아웃 중심으로 판단하기
“한 축이면 Flexbox, 두 축이면 Grid”를 더 실용적으로 바꾸면 다음 질문이 된다.
콘텐츠가 먼저인가?
항목의 고유 크기를 유지하면서 남은 공간을 나누고, 항목 수에 따라 자연스럽게 흐르게 하려면 Flexbox가 잘 맞는다.
- 아이콘, 제목, 액션이 있는 toolbar
- 버튼 그룹
- avatar와 사용자 정보
- navigation link 행
- 카드 내부의 세로 배치
track 구조가 먼저인가?
항목이 어떤 열과 행에 놓일지, 서로 다른 행에서도 같은 선에 맞아야 하는지를 먼저 정한다면 Grid가 잘 맞는다.
- sidebar와 main 영역
- label과 input이 정렬되는 form
- 여러 줄 카드 gallery
- dashboard widget
- 이름·수량·가격 열이 맞아야 하는 요약 목록
| 질문 | Flexbox 쪽 신호 | Grid 쪽 신호 |
|---|---|---|
| 항목 수가 바뀌면? | 자연스럽게 흐르고 남은 공간 재분배 | 정해진 track에 배치 |
| 줄이 바뀌면? | 각 줄이 독립적으로 계산되어도 됨 | 이전 줄과 열 선을 맞춰야 함 |
| 크기 제어 위치 | item의 flex 속성이 중요 |
container의 track 정의가 중요 |
| 주된 관심 | 순서, 정렬, 공간 분배 | 행·열 관계, 영역 배치 |
Flexbox가 자연스러운 경우
Toolbar는 항목의 너비가 콘텐츠마다 다르고, 한 항목이 남은 공간을 차지하면 된다.
<div class="toolbar">
<a class="toolbar__brand" href="/">Example</a>
<label class="toolbar__search">
<span class="sr-only">검색</span>
<input type="search" />
</label>
<button type="button">알림</button>
<button type="button">프로필</button>
</div>
.toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
}
.toolbar__brand {
flex: none;
}
.toolbar__search {
flex: 1 1 20rem;
min-width: 0;
}
.toolbar__search input {
width: 100%;
}
브랜드와 버튼은 콘텐츠 크기를 유지하고 검색 영역이 남은 공간을 사용한다. 열 선을 여러 행에 맞출 요구가 없으므로 Flexbox가 단순하다.
세로 카드 내부에서도 Flexbox가 유용하다.
.product-card {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.product-card__description {
flex: 1;
}
.product-card__action {
margin-top: auto;
}
카드 높이가 같다면 설명 길이가 달라도 액션을 아래쪽에 붙일 수 있다. 여기서 주된 문제는 한 카드 내부의 세로 공간 분배다.
Grid가 자연스러운 경우
페이지 shell은 sidebar와 main이라는 두 track을 먼저 정의한다.
.page-shell {
display: grid;
grid-template-columns:
minmax(14rem, 18rem)
minmax(0, 1fr);
gap: 2rem;
}
minmax(0, 1fr)는 main 콘텐츠가 긴 문자열이나 넓은 내부 요소 때문에 track을 밀어내는 문제를 줄인다.
폼의 label과 input을 여러 행에서 맞출 때도 Grid가 자연스럽다.
<form class="settings-form">
<label for="name">이름</label>
<input id="name" name="name" />
<label for="email">이메일 주소</label>
<input id="email" name="email" type="email" />
</form>
.settings-form {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: 0.75rem 1rem;
align-items: center;
}
모든 label이 첫 번째 열을 공유하고 input이 두 번째 열을 공유한다. 각 행을 별도 Flexbox로 만들면 label 너비를 맞추기 위한 추가 규칙이 필요하다.
Grid area를 사용하면 큰 구조에 이름을 붙일 수 있다.
.dashboard {
display: grid;
grid-template:
"summary summary" auto
"chart activity" minmax(20rem, 1fr)
/ minmax(0, 2fr) minmax(16rem, 1fr);
gap: 1rem;
}
.summary {
grid-area: summary;
}
.chart {
grid-area: chart;
}
.activity {
grid-area: activity;
}
구조가 복잡하다고 무조건 Grid를 쓰는 것이 아니라 행과 열의 관계가 요구사항 자체일 때 사용한다.
여러 줄 Flexbox와 Grid의 결정적인 차이
카드를 wrapping된 Flexbox로 배치해 보자.
.cards {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card {
flex: 1 1 15rem;
}
첫째 줄에 카드 세 개, 둘째 줄에 카드 하나가 남으면 마지막 카드가 줄 전체 너비로 늘어날 수 있다. 각 flex line은 남은 공간을 독립적으로 나누기 때문이다.
┌───────┬───────┬───────┐
│ A │ B │ C │
├───────────────────────┤
│ D │
└───────────────────────┘
이 결과가 태그 목록이나 버튼 행에서는 자연스러울 수 있다. 카드 gallery에서 앞줄과 같은 열 너비를 유지하려면 Grid가 더 적합하다.
.cards {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
gap: 1rem;
}
┌───────┬───────┬───────┐
│ A │ B │ C │
├───────┼───────┼───────┤
│ D │ │ │
└───────┴───────┴───────┘
Grid item D는 자신의 cell에 남고 다른 행과 열 선을 공유한다.
auto-fit과 auto-fill의 차이도 마지막 행에서 드러난다.
auto-fit: 비어 있는 반복 track을 접고 기존 항목이 공간을 늘려 쓴다.auto-fill: 들어갈 수 있는 빈 track도 유지한다.
대부분의 반응형 gallery에서는 auto-fit이 기대에 가깝지만, 빈 column 구조를 유지해야 한다면 auto-fill이 필요할 수 있다.
남은 공간과 항목 크기가 계산되는 방식
Flexbox를 제대로 사용하려면 flex shorthand가 무엇을 의미하는지 알아야 한다.
.item {
flex: 1;
}
일반적인 브라우저 동작에서 이는 항목들이 0에 가까운 basis에서 시작해 남은 공간을 grow factor에 따라 나누도록 만든다. 반면 flex: auto는 콘텐츠 또는 지정된 main size를 basis로 삼고 남은 공간을 나눈다.
| 값 | 개념적인 의미 | 사용 예 |
|---|---|---|
flex: initial |
늘지 않고 필요하면 줄어듦 | 기본 콘텐츠 크기 유지 |
flex: auto |
콘텐츠 크기 기준으로 늘고 줄어듦 | 내용 비율을 반영한 유연 항목 |
flex: none |
늘지도 줄지도 않음 | 고정 크기 액션 |
flex: 1 |
같은 basis에서 남은 공간 비례 분배 | 같은 폭 column |
무조건 flex: 1을 넣으면 제목이 긴 항목과 짧은 항목의 콘텐츠 특성이 무시될 수 있다. 원하는 것이 같은 폭인지, 콘텐츠 기반의 유연한 폭인지 먼저 결정한다.
Grid의 fr도 “전체 컨테이너의 일정 비율”이라고만 이해하면 문제가 생긴다.
.layout {
grid-template-columns: 1fr 1fr;
}
긴 비분리 문자열이 있으면 track의 자동 최소 크기 때문에 한 열이 예상보다 커질 수 있다. 축소 가능한 동일 열이 목적이면 다음이 더 명시적이다.
.layout {
grid-template-columns:
minmax(0, 1fr)
minmax(0, 1fr);
}
브라우저는 콘텐츠의 min-content와 max-content 크기를 레이아웃 계산에 사용한다. 레이아웃이 안 줄어드는 문제를 만났을 때 width 숫자를 늘리기 전에 자동 최소 크기를 확인해야 한다.
자주 만나는 overflow 문제
Flex item의 기본 min-width: auto는 많은 경우 콘텐츠의 최소 크기보다 작아지지 않게 한다. 긴 URL이나 white-space: nowrap 텍스트가 container를 밀어낼 수 있다.
<div class="message">
<img class="message__avatar" src="/avatar.png" alt="" />
<div class="message__body">
<strong>긴 사용자 이름</strong>
<p class="message__text">아주긴문자열이공백없이계속이어지는경우</p>
</div>
</div>
.message {
display: flex;
gap: 0.75rem;
}
.message__avatar {
flex: none;
width: 2.5rem;
height: 2.5rem;
}
.message__body {
min-width: 0;
}
.message__text {
overflow-wrap: anywhere;
}
min-width: 0은 body가 flex container 안에서 실제 available space까지 줄어들도록 허용한다. 이후 텍스트 정책에 맞게 줄바꿈이나 ellipsis를 정한다.
한 줄 말줄임은 다음 조건이 함께 필요하다.
.message__title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
Grid에서도 minmax(0, 1fr) 또는 item의 min-width: 0이 같은 종류의 문제를 해결한다.
Flexbox를 Grid로 바꿔 우연히 해결하기 전에 intrinsic minimum size와 콘텐츠 줄바꿈 정책을 확인한다. 같은 문제가 Grid에서도 나타날 수 있다.
반응형 카드 목록 설계하기
고정 breakpoint로 column 수를 하나씩 지정할 수 있다.
.gallery {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 48rem) {
.gallery {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 72rem) {
.gallery {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
제품 디자인이 특정 column 수와 breakpoint를 요구하면 이 방식이 명확하다.
카드의 최소 읽기 폭만 정해져 있고 가능한 만큼 자동으로 배치하려면 다음 패턴이 유용하다.
.gallery {
--card-min: 16rem;
display: grid;
grid-template-columns:
repeat(
auto-fit,
minmax(min(100%, var(--card-min)), 1fr)
);
gap: clamp(0.75rem, 2vw, 1.5rem);
}
min(100%, var(--card-min))는 container가 16rem보다 좁을 때 최소 track 때문에 가로 overflow가 생기는 것을 피한다.
어떤 방식을 고를지는 디자인 요구에 따라 다르다.
- 정확히 1·2·3열이어야 한다면 media query
- 최소 카드 폭만 보장하면 된다면 auto-fit
- 컴포넌트가 페이지가 아닌 자신의 container 폭에 반응해야 한다면 container query 검토
브라우저 폭 숫자를 먼저 정하기보다 콘텐츠가 읽기 어려워지거나 컨트롤이 겹치는 실제 파손 지점을 기준으로 전환한다.
Grid 안에 Flexbox를 중첩하기
실제 UI에서는 하나를 선택해 끝내지 않는다. 페이지와 카드 목록은 Grid, 카드 내부 action은 Flexbox가 자연스러울 수 있다.
<main class="catalog">
<aside class="filters">필터</aside>
<section class="products">
<article class="product-card">
<div class="product-card__content">
<h2>상품 이름</h2>
<p>상품 설명</p>
</div>
<div class="product-card__actions">
<span>12,000원</span>
<button type="button">담기</button>
</div>
</article>
</section>
</main>
.catalog {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr);
gap: 2rem;
}
.products {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
}
.product-card {
display: flex;
flex-direction: column;
}
.product-card__content {
flex: 1;
}
.product-card__actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
각 container가 해결하는 문제가 다르다.
.catalog: sidebar와 main의 열 관계.products: 여러 카드의 행·열 정렬.product-card: 카드 내부 세로 공간 분배.product-card__actions: 가격과 버튼의 한 축 정렬
“프로젝트는 Grid로 통일한다” 같은 규칙보다 container별 레이아웃 문제를 설명하는 편이 낫다.
시각적 순서와 DOM 순서를 분리하지 않기
Flexbox의 order와 Grid의 명시적 배치로 시각적 위치를 바꿀 수 있지만 DOM 순서와 키보드 포커스 순서가 자동으로 따라오지는 않는다.
.checkout__summary {
order: -1;
}
화면에서 요약이 먼저 보여도 스크린 리더와 Tab 순서는 DOM의 다른 콘텐츠를 먼저 만날 수 있다. 의미 있는 순서라면 HTML 구조 자체를 바꾸고 CSS는 그 순서를 시각화하는 방향으로 사용한다.
반응형에서 sidebar를 위나 아래로 옮길 때도 같은 기준을 적용한다.
- 읽기 순서가 viewport마다 정말 달라져야 하는가?
- 키보드 포커스가 화면에서 점프하지 않는가?
- 제목과 관련 control의 관계가 유지되는가?
- DOM을 중복 렌더하고 CSS로 하나를 숨기고 있지 않은가?
이 문제는 웹 접근성을 위한 키보드 포커스 설계와 직접 연결된다.
선택을 위한 실무 체크리스트
마무리
Flexbox와 Grid는 경쟁 관계가 아니다. 둘 다 정렬 기능을 제공하지만 공간을 바라보는 기준이 다르다. Flexbox는 현재 축에서 항목의 콘텐츠 크기와 남은 공간을 조정한다. Grid는 컨테이너가 행과 열 track을 만들고 항목들이 그 구조를 공유하게 한다.
한 줄 toolbar, 버튼 그룹, 카드 내부처럼 콘텐츠 흐름이 중심이면 Flexbox가 단순하다. 여러 행의 column 정렬, page area, 반응형 gallery처럼 track 관계가 중심이면 Grid가 자연스럽다. 복잡한 화면에서는 Grid container 안에 여러 Flexbox container가 들어가는 구성이 오히려 가장 읽기 쉽다.
선택 기준은 컴포넌트의 크기가 아니라 정렬 관계의 차원과 공간 결정권이다. 콘텐츠가 공간을 나누면 Flexbox, 컨테이너의 행과 열이 항목을 배치하면 Grid를 먼저 떠올린다.
관련 노트
- 02. 반응형을 Tailwind로 빠르게 구현하기
- 03. Hover Focus 상태 스타일링과 접근성
- 웹 접근성을 위한 키보드 포커스 설계
- CSS Specificity를 낮게 유지하는 방법
- Tailwind에서 디자인 토큰을 유지하는 방법
- css postion